/**
 * 自动下载抖音作品。
 *
 * 使用方式：
 *
 * ```shell
 * # 下载单个作品
 * deno run -A .\src\download\douyin.js https://www.douyin.com/video/7159841260751047948
 * # 下载作品合集
 * deno run -A .\src\download\douyin.js https://www.douyin.com/user/MS4wLjABAAAAnUkphA9AakpXyAY9KiUCcnMWYdF_GfvWLaIbCWTQ9jo
 * # 打包 exe
 * deno compile -A --import-map=import-map.json -o temp/douyin-download.exe src/download/douyin.js
 * ```
 */

import { writableStreamFromWriter } from "/deno.land/std/streams/mod.ts";
import * as path from "/deno.land/std/path/mod.ts";
import { exists } from "../share/file.js";
import { log, userAgent } from "../share/common.js";
import { format } from "/deno.land/std/datetime/mod.ts";
import { bold, gray, yellow } from "/deno.land/std/fmt/colors.ts";
//import moment from "/deno.land/x/momentjs/mod.ts";

log(yellow(`抖音作品下载器 v230113T2130 @RJ`));
log(gray(`开始...`));

// 1. 从命令行参数获取作品地址
if (Deno.args.length === 0) {
  log(`没有指定作品地址！参考例子：
  1. 若执行 “douyin-download.exe https://www.douyin.com/user/MS4wLjABAAAAnUkphA9AakpXyAY9KiUCcnMWYdF_GfvWLaIbCWTQ9jo 将下载作者 “RJ7014” 所有作品。
  2. 若执行 “douyin-download.exe https://www.douyin.com/video/7159841260751047948 将下载单个视频作品 “RJ7014/221029-放下.mp4”。
  3. 若执行 “douyin-download.exe https://www.douyin.com/note/7187042604725439781 将下载单个图文作品 “RJ7014/230110-RZDT-涨跌停K变色指标”。
  `);
  log(gray(`结束`));
  Deno.exit();
}

// 2. 加载 cookie 值
const cookieFile = "douyin-cookie.txt";
if (!(await exists(cookieFile))) {
  log(`当前目录下缺少抖音 cookie 值配置文件 ${cookieFile}，
  请手动通过网页版复制 cookie 值到此文件内再重新执行。
  cookie 值必须要包含 __ac_nonce 和 __ac_signature，多了无所谓，
  从 Chrome 直接复制值过来即可，值格式为 “__ac_nonce=\${V1}; __ac_signature=\${V2}”。
  现以为您创建了一个空白的 ${cookieFile} 文件，请打开 https://www.douyin.com 页面，
  用 Chrome 开发工具复制请求的 cookie 中带 __ac_nonce 和 __ac_signature 键的值到此文件内，
  然后重新执行即可。`);

  // 创建空白的 cookie 文件
  await Deno.writeTextFile(cookieFile, "");

  log(gray(`结束`));
  Deno.exit();
}
const cookie = await Deno.readTextFile(cookieFile);
if (cookie.length === 0 || !(cookie.includes("__ac_nonce=") && cookie.includes("__ac_signature="))) {
  log(`配置文件 ${cookieFile} 的内容不符合要求，必须至少包含 __ac_nonce 和 __ac_signature！`);
  log(gray(`结束`));
  Deno.exit();
}
// console.log(`cookie=${cookie}`);

// 3. 获取视频页面
const headers = {
  "origin": "https://www.douyin.com",
  "referer": "https://www.douyin.com/",
  "user-agent": userAgent,
  "cookie": cookie,
};
const url = Deno.args[0];
const toDir = Deno.cwd();
if (url.startsWith("https://www.douyin.com/video/") || url.startsWith("https://www.douyin.com/note/")) {
  await downloadByShowId(url, toDir, { headers: headers }, true, true);
} else if (url.startsWith("https://www.douyin.com/user/")) {
  await downloadByAuthor(url, toDir, { headers: headers }, false);
} else {
  log(`视频地址格式错误，只支持如下三种格式：
  1. 作品合集 https://www.douyin.com/user/{ID} 如 https://www.douyin.com/user/MS4wLjABAAAAnUkphA9AakpXyAY9KiUCcnMWYdF_GfvWLaIbCWTQ9jo
  2. 单个视频 https://www.douyin.com/video/{ID} 如 https://www.douyin.com/video/7159841260751047948
  3. 单个图文 https://www.douyin.com/note/{ID} 如 https://www.douyin.com/note/7187042604725439781
  `);
  log(gray(`结束`));
  Deno.exit();
}
log(gray(`结束`));

function polishingName(name) {
  // 删除名称中的 # 标记及其后的字符
  const index = name.indexOf("#");
  name = index !== -1 ? name.substring(0, index) : name;

  // 删除换行符
  name = name.replace(/[\r\n]/g, "");

  // 删除最后的中文标点符号
  name = name.replace(/[。！？]$/g, "");

  return name.trim();
}

/**
 * 通过作者主页的 url 下载作者所有作品。
 * @param authorUrl 作者主页地址，格式为 `https://www.douyin.com/user/{ID}`
 * @param toDir 作品下载到的目录
 * @param fetchOptions fetch的额外配置参数
 */
async function downloadByAuthor(authorUrl, toDir, fetchOptions) {
  const userId = new URL(authorUrl).pathname.split("/").filter(Boolean).pop();
  // log(`userId=${userId}`);

  const content = await fetch(authorUrl, fetchOptions)
    .then((res) => res.text());
  // log(content);
  const matches = content.match(/<script id="RENDER_DATA" type="application\/json">([\s\S]+?)<\/script>/im);
  // log(matches);
  if (!matches || matches.length < 2) {
    log(`视频页面返回内容异常，可能是 cookie 值有问题，请尝试重新获取新的 cookie 值。`);
    log(gray(`结束`));
    Deno.exit();
  }
  const renderData = JSON.parse(decodeURIComponent(matches[1]));
  // log(`renderData=${JSON.stringify(renderData, null, 2)}`);
  // log(`renderData["37"]=${JSON.stringify(renderData["37"], null, 2)}`);
  // await Deno.writeTextFile("temp.json", JSON.stringify(renderData["37"], null, 2));

  // 获取视频列表信息 - 在 renderData["37"] 内
  const data = {};
  for (const key in renderData) {
    const v = renderData[key];
    if (v?.user?.user?.secUid === userId) {
      //await Deno.writeTextFile("douyin-author.json", JSON.stringify(v, null, 2));
      data.authorName = v.user.user.nickname; // 股票选修课
      data.total = v.user.user.awemeCount; // 作者的总作品数
      data.count = v.post.data.length; // 本页作品数
      data.hasMore = v.post.hasMore; // 1 代表有分页
      data.list = v.post.data.map((item) => {
        // 删除名称中的一些不必要的标记字符
        let name = polishingName(item.desc);
        // 名称附加 yyMMdd 日期前缀
        name = format(new Date(item.createTime * 1000), "yyMMdd-") + name;

        // 短视频的 mp4 地址或图文的 mp3 地址
        let mainUrl = item.video.playAddr[0].src;
        if (mainUrl.startsWith("//")) mainUrl = "https:" + mainUrl;

        // 组合有用的信息
        return {
          id: item.awemeId, // 7176605280963185980
          createTime: item.createTime,
          // 类型：68-图文、61-短视频、0-抖音年度报告
          type: item.awemeType,
          isImages: item.awemeType === 68,
          name: name,
          ext: item.awemeType === 68 ? ".mp3" : ".mp4", // 图文 mp3 其余当 mp4
          // 短视频的 mp4 地址或图文的 mp3 地址
          // 1. 对于图文的 mp3 地址，url 最后的路径参数带有 `.mp3` 扩展名，可作为下载文件的扩展名
          // 2. 对于短视频 url 路径参数没有视频扩展名标识，但查询参数中有 `mime_type=video_mp4` 标识是 mp4 类型
          url: mainUrl,
          // 对于图文，此为对应的图片列表地址，url 最后的路径参数带有 `.webp` 或 `.jpeg` 扩展名，可作为下载文件的扩展名
          imageUrls: item.images.map((item, index) => {
            const imageUrl = item.urlList[0];
            const lastPath = new URL(imageUrl).pathname.split("/").filter(Boolean).pop();
            const i = lastPath.lastIndexOf(".");
            return {
              sn: index < 9 ? `0${index + 1}` : `${index + 1}`,
              ext: i !== -1 ? lastPath.substring(i) : ".webp", // 默认按 .webp
              url: imageUrl,
            };
          }),
        };
      });
      break;
    }
  }
  //console.log(`data=${JSON.stringify(data, null, 2)}`);
  log(gray(`作品作者: ${data.authorName}`));

  // TODO 分页处理
  // if (data.hasMore) { ... }

  // 3. 循环下载作者的各个作品
  const toShowDir = path.join(toDir, data.authorName);
  log(bold(`该作者共有 ${data.total} 个作品，开始下载第一页的 ${data.list.length} 个作品...`));
  let i = 0;
  for await (const item of data.list) {
    log(`开始下载 [${item.isImages ? "图文" : "视频"}] ${++i}/${data.list.length} “${item.name}”`);
    await fetchSingleShow(item, toShowDir, fetchOptions, false);
  }
  if (data.total > data.list.length) {
    log(
      gray(
        `已下载作者第一页的 ${data.list.length} 个作品，还有 ${
          data.total - data.list.length
        } 个作品因分页技术问题未解决未下载。`,
      ),
    );
  }
}

/**
 * 通过单个作品的 url 下载单个作品。
 * @param showUrl 作品 url
 * @param toDir 作品下载到的目录
 * @param fetchOptions fetch的额外配置参数
 * @param createAuthorDir 是否创建作者名称子目录
 * @param debug 是否打印调试信息
 * 下载单个作品。
 * 参数 videoUrl 为网站的视频播放页面的地址，格式为 “https://www.douyin.com/video/7176605280963185980”。
 */
async function downloadByShowId(showUrl, toDir, fetchOptions, createAuthorDir, debug) {
  if (debug) log(`抓取作品主页...${showUrl}`);
  const showId = new URL(showUrl).pathname.split("/").filter(Boolean).pop();
  // log(`showId=${showId}`);

  // 1. 获取作品页面
  const content = await fetch(showUrl, fetchOptions)
    .then((res) => res.text());
  // log(content);
  const matches = content.match(/<script id="RENDER_DATA" type="application\/json">([\s\S]+?)<\/script>/im);
  // log(matches);
  if (!matches || matches.length < 2) {
    log(`作品页面返回内容异常，可能是 cookie 值有问题，请尝试重新获取新的 cookie 值。`);
    log(gray(`结束`));
    Deno.exit();
  }
  const renderData = JSON.parse(decodeURIComponent(matches[1]));
  // log(`renderData["41"]=${JSON.stringify(renderData["41"], null, 2)}`);

  // 2. 获取作品信息 - 在 renderData["41"] 内
  let show;
  for (const key in renderData) {
    const v = renderData[key];
    if (v.awemeId === showId) {
      const detail = v.aweme.detail;
      //await Deno.writeTextFile("douyin-show.json", JSON.stringify(v, null, 2));
      // 删除名称中的一些不必要的标记字符
      let name = polishingName(detail.desc);
      // 名称附加 yyMMdd 日期前缀
      name = format(new Date(detail.createTime * 1000), "yyMMdd-") + name;

      // 短视频的 mp4 地址或图文的 mp3 地址
      let mainUrl = detail.video.playAddr[0].src;
      if (mainUrl.startsWith("//")) mainUrl = "https:" + mainUrl;

      // 组合有用的信息
      show = {
        id: showId, // 7159841260751047948
        createTime: detail.createTime,
        //createTime: format(new Date(v.aweme.detail.createTime * 1000), "yyyy-MM-ddTHH:mm:ss"),
        // 类型：68-图文、61-短视频、0-抖音年度报告
        type: detail.awemeType,
        isImages: detail.awemeType === 68,
        authorName: detail.authorInfo.nickname, // 作者名称
        name: name,
        ext: detail.awemeType === 68 ? ".mp3" : ".mp4", // 图文 mp3 其余当 mp4
        // 短视频的 mp4 地址或图文的 mp3 地址
        // 1. 对于图文的 mp3 地址，url 最后的路径参数带有 `.mp3` 扩展名，可作为下载文件的扩展名
        // 2. 对于短视频 url 路径参数没有视频扩展名标识，但查询参数中有 `mime_type=video_mp4` 标识是 mp4 类型
        url: mainUrl,
        // 对于图文，此为对应的图片列表地址，url 最后的路径参数带有 `.webp` 或 `.jpeg` 扩展名，可作为下载文件的扩展名
        imageUrls: detail.images.map((item, index) => {
          const imageUrl = item.urlList[0];
          const lastPath = new URL(imageUrl).pathname.split("/").filter(Boolean).pop();
          const i = lastPath.lastIndexOf(".");
          return {
            sn: index < 9 ? `0${index + 1}` : `${index + 1}`,
            ext: i !== -1 ? lastPath.substring(i) : ".webp", // 默认按 .webp
            url: imageUrl,
          };
        }),
      };
      break;
    }
  }
  //console.log(`show=${JSON.stringify(show, null, 2)}`);

  // 3. 下载作品
  let toDir2 = toDir;
  if (createAuthorDir) toDir2 = path.join(toDir2, show.authorName);
  await fetchSingleShow(show, toDir2, fetchOptions, true);
}

/**
 * 下载单个作品。
 * @param item 作品的下载配置信息，格式为 `{name,ext,isImages,url,imageUrls}`
 * @param toDir 作品下载到的目录
 * @param fetchOptions fetch的额外配置参数
 * @param debug 是否打印调试信息
 */
async function fetchSingleShow(item, toDir, fetchOptions, debug) {
  // 是图文就保存到子目录，否则直接保存视频到目录
  toDir = item.isImages ? path.join(toDir, item.name) : toDir;

  // 1. 创建下载到的目录
  if (!(await exists(toDir))) await Deno.mkdir(toDir, { recursive: true });

  // 2. 下载：视频保存为文件、图文保存到子目录
  // 如果视频或图文已经存在则忽略，不重复下载
  const mainFileName = path.join(toDir, item.isImages ? `配乐${item.ext}` : `${item.name}${item.ext}`);
  const targetType = item.isImages ? "图文" : "视频";
  if (!(await exists(mainFileName))) {
    if (debug) log(`下载${targetType} “${item.name}”...`);

    // 下载短视频或图文的配乐
    await fetchUrlToFile(
      item.url,
      mainFileName,
      fetchOptions,
    );
    if (debug && item.isImages) log(`  下载图文的配乐到 “${mainFileName}”`);

    // 下载图文的图片
    if (item.isImages) {
      for await (const image of item.imageUrls) {
        // console.log(JSON.stringify(image));
        const toFile = path.join(toDir, `${image.sn}${image.ext}`);
        await fetchUrlToFile(
          image.url,
          toFile,
          // 下载图片不需要 cookie，所有认证信息都已包含在 url 参数中
          { headers: { "user-agent": userAgent } },
        );
        if (debug) log(`  下载图文的图片 “${image.sn}${image.ext}” 到 “${toFile}”`);
      }
    }

    if (debug) log(`保存${targetType} “${item.name}” 到 ${item.isImages ? toDir : mainFileName}`);
  } else {
    log(`${targetType}已经存在，忽略不重复下载！“${item.name}”`);
  }
}

/**
 * 下载远程文件。
 * @param fromUrl 远程地址
 * @param toFile 下载后保存为的文件的全路径名
 * @param fetchOptions fetch的额外配置参数
 */
async function fetchUrlToFile(fromUrl, toFile, fetchOptions) {
  const response = await fetch(fromUrl, fetchOptions);
  if (response.body) {
    const file = await Deno.open(toFile, { write: true, create: true });
    const writableStream = writableStreamFromWriter(file);
    await response.body.pipeTo(writableStream);
  }
}
